/**
 * BibSonomy-Database - Database for BibSonomy.
 *
 * Copyright (C) 2006 - 2016 Knowledge & Data Engineering Group,
 *                               University of Kassel, Germany
 *                               http://www.kde.cs.uni-kassel.de/
 *                           Data Mining and Information Retrieval Group,
 *                               University of Würzburg, Germany
 *                               http://www.is.informatik.uni-wuerzburg.de/en/dmir/
 *                           L3S Research Center,
 *                               Leibniz University Hannover, Germany
 *                               http://www.l3s.de/
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.bibsonomy.database.managers;

import static org.bibsonomy.util.ValidationUtils.present;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bibsonomy.common.enums.GroupID;
import org.bibsonomy.common.enums.PostUpdateOperation;
import org.bibsonomy.common.errors.DuplicatePostErrorMessage;
import org.bibsonomy.common.errors.ErrorMessage;
import org.bibsonomy.common.errors.UpdatePostErrorMessage;
import org.bibsonomy.common.exceptions.ObjectNotFoundException;
import org.bibsonomy.database.common.AbstractDatabaseManager;
import org.bibsonomy.database.common.DBSession;
import org.bibsonomy.database.common.enums.ConstantID;
import org.bibsonomy.database.managers.chain.Chain;
import org.bibsonomy.database.params.GoldStandardReferenceParam;
import org.bibsonomy.database.params.ResourceParam;
import org.bibsonomy.database.plugin.DatabasePluginRegistry;
import org.bibsonomy.model.GoldStandard;
import org.bibsonomy.model.Post;
import org.bibsonomy.model.Resource;
import org.bibsonomy.model.User;
import org.bibsonomy.model.enums.GoldStandardRelation;
import org.bibsonomy.model.util.PostUtils;
import org.bibsonomy.services.searcher.ResourceSearch;
import org.bibsonomy.util.ReflectionUtils;

/**
 * Used to create, read, update and delete gold standard posts from the database
 *
 * @param <RR> the resource class of the reference class of <R>
 * @param <R> the resource class that is managed by this class
 * @param <P>
 *
 * @author dzo
 */
public abstract class GoldStandardDatabaseManager<RR extends Resource, R extends Resource & GoldStandard<RR>, P extends ResourceParam<RR>> extends AbstractDatabaseManager implements CrudableContent<R, P> {
	private static final Log log = LogFactory.getLog(GoldStandardDatabaseManager.class);

	/** simple class name of the resource managed by the class */
	protected final String resourceClassName;

	protected final DatabasePluginRegistry plugins;

	private final GeneralDatabaseManager generalManager;

	private ResourceSearch<R> search;

	private Chain<List<Post<R>>, P> chain;

	protected GoldStandardDatabaseManager() {
		this.resourceClassName = this.getResourceClassName();
		this.plugins = DatabasePluginRegistry.getInstance();

		this.generalManager = GeneralDatabaseManager.getInstance();
	}

	/**
	 * @return the searcher
	 */
	public ResourceSearch<R> getSearch() {
		return this.search;
	}

	/**
	 * @param search the search to set
	 */
	public void setSearch(final ResourceSearch<R> search) {
		this.search = search;
	}

	/**
	 * @return the simple class name of the second generic param (<R>, Resource)
	 */
	protected String getResourceClassName() {
		return ReflectionUtils.getActualClassArguments(this.getClass()).get(1).getSimpleName();
	}

	@Override
	public Post<R> getPostDetails(final String loginUserName, final String resourceHash, final String userName, final List<Integer> visibleGroupIDs, final DBSession session) {
		if (present(userName)) {
			return null; // TODO: think about this return
		}
		
		final Post<R> post = this.getGoldStandardPostByHash(resourceHash, session);
		
		if (present(post)) {
			final R goldStandard = post.getResource();
			/*
			 * set citation graph
			 */
			goldStandard.addAllToReferences(this.getReferencesForPost(resourceHash, session));
			goldStandard.addAllToReferenceThisPublicationIsPublishedIn(this.getReferenceThisPublicationIsPublishedIn(resourceHash, session));
			goldStandard.addAllToReferencedBy(this.getRefencedByForPost(resourceHash, session));
			goldStandard.addAllToReferencePartOfThisPublication(this.getReferencePartOfThisPublication(resourceHash, session));
		} else {
			log.debug("gold standard post with interhash '" + resourceHash + "' not found.");
		}

		return post;
	}

	@SuppressWarnings("unchecked")
	protected Post<R> getGoldStandardPostByHash(final String resourceHash, final DBSession session) {
		final P param = this.createResourceParam(resourceHash);
		return (Post<R>) this.queryForObject("getGoldStandardByHash", param, session);
	}

	private P createResourceParam(final String resourceHash) {
		final P param = this.createNewParam();
		param.setHash(resourceHash);
		return param;
	}

	@SuppressWarnings("unchecked")
	protected Set<RR> getRefencedByForPost(final String resourceHash, final DBSession session) {
		final P param = this.createResourceParam(resourceHash);
		param.setRelation(GoldStandardRelation.REFERENCE);
		return new HashSet<RR>((Collection<? extends RR>) this.queryForList("getGoldStandardRelatedBy", param, session));
	}

	@SuppressWarnings("unchecked")
	protected Set<RR> getReferencePartOfThisPublication(final String resourceHash, final DBSession session) {
		final P param = this.createResourceParam(resourceHash);
		param.setRelation(GoldStandardRelation.PART_OF);
		return new HashSet<RR>((Collection<? extends RR>) this.queryForList("getGoldStandardRelatedBy", param, session));
	}

	@SuppressWarnings("unchecked")
	private Set<RR> getReferenceThisPublicationIsPublishedIn(final String resourceHash, final DBSession session) {
		final P param = this.createResourceParam(resourceHash);
		param.setRelation(GoldStandardRelation.PART_OF);
		return new HashSet<RR>((Collection<? extends RR>) this.queryForList("getGoldStandardRelated", param, session));
	}

	@SuppressWarnings("unchecked")
	protected Set<RR> getReferencesForPost(final String interHash, final DBSession session) {
		final P param = this.createResourceParam(interHash);
		param.setRelation(GoldStandardRelation.REFERENCE);
		return new HashSet<RR>((Collection<? extends RR>) this.queryForList("getGoldStandardRelated", param, session));
	}

	@Override
	public List<Post<R>> getPosts(final P param, final DBSession session) {
		return this.chain.perform(param, session);
	}

	/**
	 * @param chain the chain to set
	 */
	public void setChain(final Chain<List<Post<R>>, P> chain) {
		this.chain = chain;
	}

	@Override
	public boolean createPost(final Post<R> post, final User loggedinUser, final DBSession session) {
		session.beginTransaction();
		try {
			final String resourceHash = post.getResource().getInterHash();

			final Post<R> newPostInDB = this.getGoldStandardPostByHash(resourceHash, session);

			if (present(newPostInDB)) {
				log.debug("gold stanard post with hash \"" + resourceHash + "\" already exists in DB");
				final ErrorMessage errorMessage = new DuplicatePostErrorMessage(this.resourceClassName, resourceHash);
				session.addError(PostUtils.getKeyForCommunityPost(post), errorMessage);
				session.commitTransaction();
				return false;
			}

			post.setContentId(this.generalManager.getNewId(ConstantID.IDS_CONTENT_ID, session));

			this.onGoldStandardCreate(resourceHash, session);
			this.insertPost(post, session);

			session.commitTransaction();
		} finally {
			session.endTransaction();
		}

		return true;
	}

	protected void insertPost(final Post<R> post, final DBSession session) {
		final P insertParam = this.getInsertParam(post);
		this.insert("insert" + this.resourceClassName, insertParam, session);
	}


	@SuppressWarnings("unchecked") // XXX: java generics :(
	protected P getInsertParam(final Post<R> post) {
		final P insert = this.createNewParam();

		insert.setResource((RR) post.getResource());
		insert.setDescription(post.getDescription());
		insert.setDate(post.getDate());
		insert.setRequestedContentId(post.getContentId().intValue());
		insert.setUserName(present(post.getUser()) ? post.getUser().getName() : "");
		insert.setGroupId(GroupID.PUBLIC); // gold standards are public
		insert.setApproved(post.getApproved());

		return insert;
	}

	// TODO: remove method!
	protected abstract P createNewParam();

	@Override
	public boolean updatePost(final Post<R> post, final String oldHash, final User loginUser, final PostUpdateOperation operation, final DBSession session) {
		session.beginTransaction();
		try {

			/*
			 * the current interhash of the resource
			 */
			final R resource = post.getResource();
			resource.recalculateHashes();

			final String resourceHash = resource.getInterHash();
			/*
			 * the resource with the "old" interhash, that was sent
			 * within the update resource request
			 */
			final Post<R> oldPost;
			if (present(oldHash)) {
				// if yes, check if a post exists with the old interhash
				oldPost = this.getGoldStandardPostByHash(oldHash, session);
				/*
				 * check if post to update is in db
				 */
				if (!present(oldPost)) {
					final String hash = resource.getInterHash();
					/*
					 * not found -> add ErrorMessage
					 */
					final ErrorMessage errorMessage = new UpdatePostErrorMessage(this.resourceClassName, hash);
					session.addError(PostUtils.getKeyForCommunityPost(post), errorMessage);
					log.warn("Added UpdatePostErrorMessage for post " + post.getResource().getIntraHash());
					session.commitTransaction();

					return false;
				}
			} else {
				throw new IllegalArgumentException("Could not update standard post: no interhash specified.");
			}

			/*
			 * check for possible duplicates
			 */
			final Post<R> newPostInDB = this.getGoldStandardPostByHash(resourceHash, session);

			if (present(newPostInDB) && !oldHash.equals(resourceHash)) {
				log.debug("gold stanard post with hash \"" + resourceHash + "\" already exists in DB");
				final ErrorMessage errorMessage = new DuplicatePostErrorMessage(this.resourceClassName, resourceHash);
				session.addError(resourceHash, errorMessage);

				session.commitTransaction();

				return false;
			}

			final int newContentId = this.generalManager.getNewId(ConstantID.IDS_CONTENT_ID, session).intValue();
			post.setContentId(Integer.valueOf(newContentId));

			// first log the gold standard
			this.onGoldStandardUpdate(oldPost.getContentId().intValue(), newContentId, oldHash, resourceHash, session);
			// logs old post and updates reference table
			// then you can delete it
			this.deletePost(oldHash, true, session);
			// and add a new one
			this.insertPost(post, session);

			session.commitTransaction();
		} finally {
			session.endTransaction();
		}
		return true;
	}

	@Override
	public boolean deletePost(final String userName, final String resourceHash, final User loggedinUser, final DBSession session) {
		if (present(userName)) {
			return false;
		}
		return this.deletePost(resourceHash, false, session);
	}

	protected boolean deletePost(final String resourceHash, final boolean update, final DBSession session) {
		session.beginTransaction();
		try {
			final Post<R> post = this.getGoldStandardPostByHash(resourceHash, session);

			if (!present(post)) {
				log.debug("gold stanard post with hash \"" + resourceHash + "\" not found");
				return false;
			}

			if (!update) {
				this.onGoldStandardDelete(resourceHash, session);
			}

			final P param = this.createNewParam();
			param.setHash(resourceHash);

			this.delete("deleteGoldStandard", param, session);
			session.commitTransaction();
		} finally {
			session.endTransaction();
		}

		return true;
	}

	protected GoldStandardReferenceParam createParam(final Post<R> post) {
		final GoldStandardReferenceParam param = new GoldStandardReferenceParam();
		param.setHash(post.getResource().getInterHash());
		param.setUsername(post.getUser().getName());

		return param;
	}

	/**
	 * adds references to a standard post
	 *
	 * @param userName TODO: currently unused
	 * @param interHash
	 * @param references
	 * @param relation
	 * @param session
	 */
	public void addRelationsToPost(final String userName, final String interHash, final Set<String> references, final GoldStandardRelation relation, final DBSession session) {
		session.beginTransaction();
		try {
			final Post<R> post = this.getGoldStandardPostByHash(interHash, session);
			if (!present(post)) {
				log.debug("gold standard post with interhash '" + interHash + "'  not found");
				throw new ObjectNotFoundException(interHash);
			}

			final GoldStandardReferenceParam param = this.createParam(post);
			if (present(references)) {
				// TODO: A <-> A references and duplicate references
				for (final String referenceHash : references) {
					final Post<R> refPost = this.getGoldStandardPostByHash(referenceHash, session);
					if (present(refPost)) {
						param.setRefHash(referenceHash);
						param.setRelation(relation);
						this.insert("insertGoldStandardRelation", param, session);
					} else {
						log.info("Can't add reference. Gold standard " + this.resourceClassName + " reference with resourceHash " + referenceHash + " not found.");
					}
				}
			}
			session.commitTransaction();
		} finally {
			session.endTransaction();
		}

	}

	/**
	 * removes references from a standard post
	 *
	 * @param userName
	 * @param interHash
	 * @param references
	 * @param relation
	 * @param session
	 */
	public void removeRelationsFromPost(final String userName, final String interHash, final Set<String> references, final GoldStandardRelation relation, final DBSession session) {
		session.beginTransaction();
		try {
			final Post<R> post = this.getGoldStandardPostByHash(interHash, session);
			if (!present(post)) {
				log.debug("gold standard post with interhash '" + interHash + "'  not found");
				return;
			}

			final GoldStandardReferenceParam param = this.createParam(post);
			if (present(references)) {
				for (final String referenceHash : references) {
					final Post<R> refPost = this.getGoldStandardPostByHash(referenceHash, session);
					if (present(refPost)) {
						param.setRefHash(referenceHash);
						param.setRelation(relation);
						this.onGoldStandardRelationDelete(userName, interHash, referenceHash, relation, session);
						this.delete("deleteGoldStandardRelation", param, session);
					} else {
						log.info("Can't remove reference. Gold standard " + this.resourceClassName +  " reference with resourceHash " + referenceHash + " not found.");
					}
				}
			}

			session.commitTransaction();
		} finally {
			session.endTransaction();
		}
	}

	private void onGoldStandardCreate(final String resourceHash, final DBSession session) {
		this.plugins.onGoldStandardCreate(resourceHash, session);
	}

	private void onGoldStandardUpdate(final int oldContentId, final int newContentId, final String oldHash, final String newResourceHash, final DBSession session) {
		this.plugins.onGoldStandardUpdate(oldContentId, newContentId, newResourceHash, oldHash, session);
	}

	private void onGoldStandardDelete(final String resourceHash, final DBSession session) {
		this.plugins.onGoldStandardDelete(resourceHash, session);
	}

	/**
	 *
	 * @param userName
	 * @param interHash
	 * @param interHashRef
	 * @param interHashRelation
	 * @param session
	 */
	protected abstract void onGoldStandardRelationDelete(final String userName, final String interHash, final String interHashRef, final GoldStandardRelation interHashRelation, final DBSession session);
}